Skip to content

SpringCloud面经[上]

微服务通讯方式有哪些?

微服务的通讯方式主要有以下几种:

  1. RESTful API:基于 HTTP 协议的 RESTful API 是最常用的微服务通讯方式之一。服务之间通过 HTTP 请求和响应进行通讯,实现数据交换。这种方式简单、通用,适用于各种场景,但可能不适合对实时性要求非常高的场景。
  2. RPC(远程过程调用):RPC 允许一个服务像调用本地方法一样调用另一个服务的方法。它通过将方法调用封装成网络数据包并在不同的进程之间传输,实现不同服务之间的互相调用。RPC 方式可以提高调用的效率和性能,但可能需要更多的配置和管理工作。
  3. 消息队列通讯:如 RabbitMQ、Kafka、RocketMQ 等,服务之间不直接调用,而是通过消息队列进行异步消息传递,实现服务之间的解耦和异步处理。
  4. 事件驱动通讯:服务之间通过事件触发通讯,一旦某个服务发生了某个事件,就会触发其他服务的响应。这种方式可以实现服务的松耦合和事件的实时处理,典型的实现如 Event Bus。
  5. WebSocket(长连接通信):使用 WebSocket 实现双向通信,常用于实时推送场景,服务间可以维持长期的 TCP 连接进行数据交换。

其中,RESTful API 和 RPC 是微服务间最常用的通讯方式,但它们的使用场景又略有不同:

  • RESTful API 通常用于外部接口或第三方接口通讯。
  • RPC 通常用于内部微服务之间的方法调用。

RESTful API VS RPC

它们的区别主要体现在以下几点:

  1. 功能和用途不同
    1. RESTful API 常用于浏览器和服务器之间的通信,第三方接口通讯等,它可以实现基于请求-响应模式的通信,支持无状态和有状态的交互。
    2. RPC 是一种用于远程过程调用的协议,用于不同计算节点之间的通信,多用于微服务内部间的调用。它允许应用程序通过网络调用远程服务,并像调用本地方法一样轻松实现分布式系统的集成。
  2. 数据格式不同
    1. RESTful API 使用文本格式来传输数据,通常使用 JSON 或 XML 进行序列化。
    2. RPC 通常使用二进制格式来传输数据,例如 Protocol Buffers(ProtoBuf)或 Apache Thrift。
  3. 性能不同:RPC 通常比 RESTful API 更高效。这是因为 RPC 的协议设计更加轻量级,并且它可以对传输的数据进行二进制压缩,使得请求报文体积更小,从而提高传输效率。而 RESTful API 基于 HTTP 协议,其报文头等信息可能使得传输的数据量相对较大,传输效率较低。

RESTful API通讯实现

RESTful API 目前主流的实现方式有以下两种:

  • RestTemplate:Spring 内置的用于执行 HTTP 请求的类。

  • Spring Cloud OpenFegin:OpenFeign 是 Spring Cloud 对 Feign 库的封装,提供声明式的 HTTP 客户端,简化了服务调用的编码工作。

针对SpringBoot使用远程调用HTTP可参考:

RestTemplate使用

java
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
    return new RestTemplate();
}

// 使用时
@Autowired
private RestTemplate restTemplate;

public void callOtherService(String serviceName) {
    String url = "http://" + serviceName + "/api/path";
    ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
}

Spring Cloud OpenFegin 使用

OpenFegin 引入到项目之后,需要先在 Spring Boot 启动类上添加 @EnableFeignClients 注解,之后使用以下代码就可以实现 RESTful 通讯了:

java
import org.springframework.cloud.openfeign.FeignClient;  
import org.springframework.web.bind.annotation.GetMapping;  
  
@FeignClient(name = "service-provider")  
public interface ServiceProviderClient {  
  
    @GetMapping("/api/hello")  
    String hello();  
}

RPC通讯实现

RPC 目前主流的通讯方式有以下两种:

  • Dubbo:阿里巴巴公司开源的一个 Java 高性能优秀的服务框架,它基于 TCP 或 HTTP 的 RPC 远程过程调用,支持负载均衡和容错,自动服务注册和发现。
  • gRPC:Google 开发的高性能、通用的开源 RPC 框架,它主要面向移动应用开发并基于 HTTP/2 协议标准设计。gRPC 使用 ProtoBuf(Protocol Buffers)作为序列化工具和接口定义语言,要求在调用前需要先定义好接口契约,并使用工具生成代码,然后在代码中调用这些生成的类进行服务调用。

说一下Gateway过滤器的分类有哪些?

在 Spring Cloud Gateway 中,过滤器是在请求到达目标服务之前或之后,执行某些特定操作的一种机制。例如,它可以实现对传入的请求进行验证、修改、日志记录、身份验证、流量控制等各种功能。

在 Spring Cloud Gateway 中,过滤器总共分为以下两大类:

局部过滤器:只作用于某一个路由(route)。

全局过滤器:对所有的路由都有效。

  1. 内置全局过滤器:Spring Cloud Gateway 自带的 30+ 过滤器,详情请访问:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
  2. 自定义全局过滤器:开发者自行实现的过滤器。

局部过滤器

Spring Cloud Gateway 中的局部过滤器配置如下:

java
spring:
  cloud:
    gateway:
      routes:
        - id: userservice
          uri: http://192.168.1.7:56628
          predicates:
            - Path=/user/**
          filters:
            - AddResponseHeader=gateway-flag, xiaosheng.org.cn

以上过滤器的含义是在输出对象 Response 中添加 Header 信息,key 为“gateway-flag”,value 为“xiaosheng.org.cn”。

PS:AddResponseHeader 也是 Gateway 内置过滤器之一。

全局过滤器

全局过滤器会对当前网关中的所有路由都生效。

内置全局过滤器

Spring Cloud Gateway 中的内置全局过滤器配置如下:

java
spring:
  cloud:
    gateway:
      routes:
        - id: userservice
          uri: http://192.168.1.7:51627
          predicates:
          - Weight=group1,50
        - id: userservice2
          uri: http://192.168.1.7:56628
          predicates:
            - Weight=group1,50
          filters:
            - AddResponseHeader=gateway-flag, xiaosheng.org.cn
      default-filters:
        - AddResponseHeader=gateway-default-filters, xiaosheng.org.cn

其中的“default-filters”就是全局内置过滤器,它对所有的路由(route)有效,它的含义是在输出对象 Response 中添加 Header 信息,key 为“gateway-default-filters”,value 为"xiaosheng.org.cn"。

filters 部分配置了一个名为 AddResponseHeader 的过滤器,其作用是向响应中添加一个自定义的 HTTP 头。下面是对这个过滤器的详细说明:

  • 过滤器名称AddResponseHeader 是 Spring Cloud Gateway 提供的一个内置过滤器工厂(GatewayFilter Factory),用于添加响应头。
  • 参数:该过滤器接受两个参数:
    • 第一个参数 gateway-flag 是要添加到响应中的 HTTP 头的名称。
    • 第二个参数 javacn.site 是要设置的 HTTP 头的值。

当一个请求匹配到这个路由(即请求路径以 /user/ 开头)时,Spring Cloud Gateway 会在将请求转发到目标服务(在这个例子中是 http://192.168.1.7:56628)之后,在响应中添加一个名为 gateway-flag、值为 javacn.site 的 HTTP 头。

过滤器可以用于多种用途,例如:

  • 修改请求头或响应头。
  • 重定向请求。
  • 验证请求的权限。
  • 记录日志。
  • 限流。
  • 等等。

Spring Cloud Gateway 提供了多种内置的过滤器工厂,也可以自定义过滤器工厂来满足特定的需求。通过灵活使用过滤器,可以构建强大的网关逻辑,以适应不同的业务场景。

自定义全局过滤器

Spring Cloud Gateway 中自定义全局过滤器的实现是,定义一个类,使用 @Component 注解将其存入 IoC 容器,然后再实现 GlobalFilter 接口,重写 filter 方法,在 filter 中写自己的过滤方法即可,具体实现如下:

java
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class AuthFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 得到 request、response 对象
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        // 业务逻辑代码
        if(request.getQueryParams().getFirst("auth")==null){
            // 权限有问题返回,并结束执行
            response.setStatusCode(HttpStatus.FORBIDDEN);
            return response.setComplete();
        }
        // 此步骤正常,执行下一步
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        // 此值越小越早执行
        return 1;
    }
}

以上代码是验证请求参数中是否有auth参数,如果没有的话就认为未登录,调用response.setComplete()终止继续执行,反之则认为已经登录,可以执行后续流程了,使用chain.filter(exchange)来实现。

org.springframework.core.Ordered 是 Spring 框架中的一个接口,它用于定义那些需要排序的组件的顺序。在 Spring 中,有些组件可能需要按照特定的顺序执行,例如事件监听器、拦截器等。Ordered 接口提供了一个方法 getOrder(),允许实现该接口的类返回一个整数值,这个值决定了组件的优先级。

以下是 Ordered 接口的基本定义:

java
public interface Ordered {
   int getOrder();
}

实现 Ordered 接口的组件可以通过重写 getOrder() 方法来指定它们的顺序。在 Spring 容器中,具有较低顺序值的组件会优先于具有较高顺序值的组件被处理。

例如,如果你有一个实现了 Ordered 接口的事件监听器,你可以这样设置它的顺序:

java
public class MyEventListener implements Ordered {
   @Override
   public int getOrder() {
       // 返回一个较低的值以确保优先级较高
       return 1;
   }
   // 其他方法...
}

在 Spring 的不同上下文中,Ordered 接口的使用方式可能会有所不同。例如,在 Spring MVC 中,拦截器(HandlerInterceptor)可以实现 Ordered 接口来控制它们的执行顺序。

请注意,Ordered 接口是 Spring 框架的一部分,如果你正在使用 Spring 框架,你需要确保你的项目依赖了 Spring 的相关库。

限流的算法有哪些?

限流的实现算法有很多,但常见的限流算法有三种:计数器算法、漏桶算法和令牌桶算法。

计数器算法

计数器算法是在一定的时间间隔里,记录请求次数,当请求次数超过该时间限制时,就把计数器清零,然后重新计算。当请求次数超过间隔内的最大次数时,拒绝访问。

计数器算法的实现比较简单,但存在“突刺现象”。

突刺现象是指,比如限流 QPS(每秒查询率)为 100,算法的实现思路就是从第一个请求进来开始计时,在接下来的 1 秒内,每来一个请求,就把计数加 1,如果累加的数字达到了 100,后续的请求就会被全部拒绝。等到 1 秒结束后,把计数恢复成 0,重新开始计数。如果在单位时间 1 秒内的前 10 毫秒处理了 100 个请求,那么后面的 990 毫秒会请求拒绝所有的请求,我们把这种现象称为“突刺现象”。

计数器算法的简单实现代码如下:

java
import java.util.Calendar;
import java.util.Date;
import java.util.Random;

public class CounterLimit {
    // 记录上次统计时间
    static Date lastDate = new Date();
    // 初始化值
    static int counter = 0;
    // 限流方法
    static boolean countLimit() {
        // 获取当前时间
        Date now = new Date();
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(now);
        // 当前分
        int minute = calendar.get(Calendar.MINUTE);
        calendar.setTime(lastDate);
        int lastMinute = calendar.get(Calendar.MINUTE);
        if (minute != lastMinute) {
            lastDate = now;
            counter = 0;
        }
        ++counter;
        return counter >= 100; // 判断计数器是否大于每分钟限定的值。
    }
 
    // 测试方法
    public static void main(String[] args) {
        for (; ; ) {
            // 模拟一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Random random = new Random();
            int i = random.nextInt(3);
            // 模拟1秒内请求1次
            if (i == 1) {
                if (countLimit()) {
                    System.out.println("限流了" + counter);
 
                } else {
                    System.out.println("没限流" + counter);
                }
            } else if (i == 2) { // 模拟1秒内请求2次
                for (int j = 0; j < 2; j++) {
                    if (countLimit()) {
                        System.out.println("限流了" + counter);
                    } else {
                        System.out.println("没限流" + counter);
                    }
                }
            } else { // 模拟1秒内请求10次
                for (int j = 0; j < 10; j++) {
                    if (countLimit()) {
                        System.out.println("限流了" + counter);
                    } else {
                        System.out.println("没限流" + counter);
                    }
                }
            }
        }
    }
}

漏桶算法

漏桶算法的实现思路是,有一个固定容量的漏桶,水流(请求)可以按照任意速率先进入到漏桶里,但漏桶总是以固定的速率匀速流出,当流入量过大的时候(超过桶的容量),则多余水流(请求)直接溢出。如下图所示:

image-20240628175044170

漏桶算法提供了一种机制,通过它可以让突发流量被整形,以便为系统提供稳定的请求,比如 Sentinel 中流量整形(匀速排队功能)就是此算法实现的,如下图所示:

image-20240628175131501

令牌桶算法

令牌按固定的速率被放入令牌桶中,桶中最多存放 N 个令牌(Token),当桶装满时,新添加的令牌被丢弃或拒绝。当请求到达时,将从桶中删除 1 个令牌。令牌桶中的令牌不仅可以被移除,还可以往里添加,所以为了保证接口随时有数据通过,必须不停地往桶里加令牌。由此可见,往桶里加令牌的速度就决定了数据通过接口的速度。我们通过控制往令牌桶里加令牌的速度从而控制接口的流量。 令牌桶的实现原理如下图所示:

image-20240628175243571

漏桶算法 VS 令牌桶算法

漏桶算法是按照常量固定速率流出请求的,流入请求速率任意,当流入的请求数累积到漏桶容量时,新流入的请求被拒绝。令牌桶算法是按照固定速率往桶中添加令牌的,请求是否被处理需要看桶中的令牌是否足够,当令牌数减为零时,拒绝新的请求。令牌桶算法允许突发请求,只要有令牌就可以处理,允许一定程度的突发流量。漏桶算法限制的是常量流出速率,从而使突发流入速率平滑。 比如服务器空闲时,理论上使用漏桶算法服务器可以直接处理一次洪峰(一次洪水过程的最大流量),但是漏桶算法处理请求的速率是恒定的,因此,前期服务器资源只能根据恒定的漏水速度逐步处理请求,无法直接处理这次洪峰。而使用令牌桶算法就不存在这个问题,因为它可以先把令牌桶一次性装满,处理一次洪峰之后再走限流。

小结

限流的常见算法有以下 3 种:

  1. 计数器算法:实现简单,但有突刺现象;
  2. 漏桶算法:固定速率处理请求,处理任意流量更加平滑,可以实现流量整形;
  3. 令牌桶算法:通过控制桶中的令牌实现限流,可以处理一定的突发流量,比如处理一次洪峰。

网关如何实现限流?

网关(Gateway)是微服务中不可缺少的一部分,它是微服务中提供了统一访问地址的组件,充当了客户端和内部微服务之间的中介。网关主要负责流量路由和转发,将外部请求引导到相应的微服务实例上,同时提供一些功能,如身份认证、授权、限流、监控、日志记录等。

网关的主要作用有以下几个:

  1. 路由功能:网关可以根据目标地址的不同,选择最佳的路径将数据包从源网络路由到目标网络。它通过维护路由表来确定数据包的转发方向,并选择最优的路径。
  2. 安全控制(统一认证授权):网关可以实施网络安全策略,对进出的数据包进行检查和过滤。它可以验证和授权来自源网络的数据包,并阻止未经授权的访问。防火墙是一种常见的网关设备,用于过滤和保护网络免受恶意攻击和未经授权的访问。
  3. 协议转换:不同网络使用不同的通信协议,网关可以进行协议转换,使得不同网络的设备可以互相通信。例如,例如将 HTTPS 协议转换成 HTTP 协议。
  4. 网络地址转换(NAT):网关还可以执行网络地址转换,将内部网络使用的私有 IP 地址转换为外部网络使用的公共 IP 地址,以实现多台计算机共享一个公共 IP 地址出去上网。

关于限流

为了保护后端微服务免受突发高流量请求的影响,确保系统的稳定和可靠性,所以在网关层必须“限流”操作。

限流是一种流量控制的策略,用于限制系统处理请求的速率或数量,以保护系统免受过载或攻击的影响。通过限制请求的数量或速率,可以平衡系统和资源之间的压力,确保系统在可接受的范围内运行。

限流的常见策略通常有以下几种:

  1. 请求速率限流:限制单位时间内系统可以接受的最大请求数量。例如,每秒最多处理 100 个请求。当请求超过限制时,可以选择拒绝或延迟处理这些请求。
  2. 并发请求数限流:限制同时处理的请求数量。例如,限制系统只能同时处理100个并发请求。当并发请求数超过限制时,可以选择拒绝或排队等待。
  3. 用户级别限流:根据用户进行限流,限制每个用户的请求频率或数量。例如,限制每个用户每分钟只能发送 10 个请求。当用户请求超过限制时,可以选择拒绝或延迟处理。
  4. API 级别限流:根据 API 接口进行限流,限制每个接口的请求频率或数量。例如,限制某个接口每秒只能处理 50 个请求。当接口请求超过限制时,可以选择拒绝或延迟处理。

当然,我们也可以在程序中使用多种策略混合限流,以保证内部微服务的稳定性。

如何实现限流

了解了网关和限流的相关内容之后,我们以目前主流的网关组件 Spring Cloud Gateway 为例,来实现一下限流功能。

Spring Cloud Gateway 实现限流的方式有两种:

  1. 使用内置 Filter(过滤器)实现限流。
  2. 使用限流组件 Spring Cloud Alibaba Sentinel 或者 Spring Cloud Netflix Hystrix 实现限流。

那既然 Spring Cloud Gateway 中已经内置了限流功能,那我们接下来就来看 Spring Cloud Gateway 内置限流是如何实现的?

Spring Cloud Gateway 内置的限流器为 RequestRateLimiter GatewayFilter Factory,官网说明文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-requestratelimiter-gatewayfilter-factoryopen in new window

Spring Cloud Gateway 支持和 Redis 一起来实现限流功能,它的实现步骤如下:

  1. 在网关项目中添加 Redis 框架依赖
  2. 创建限流规则
  3. 配置限流过滤器

具体实现如下

添加Redis框架依赖

在项目的 pom.xml 中,添加以下配置信息(添加 Redis 框架依赖支持):

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

创建限流规则

接下来我们新建一个限流规则定义类,实现一下根据 IP 进行限流的功能,实现示例代码如下:

java
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class IpAddressKeyResolver implements KeyResolver {
    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getRemoteAddress().
                         getHostString());
    }
}

PS:当然,我们还可以通过 URL、方法名、用户等进行限流操作,只需要修改此步骤中的限流凭证,也就是 KeyResolver 即可。

配置限流过滤器

在网关项目的配置文件中,添加以下配置信息:

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: retry
          uri: lb://nacos-discovery-demo
          predicates:
            - Path=/retry/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 1
                keyResolver: '#{@ipAddressKeyResolver}' # spEL表达式
  data:
    redis:
      host: 127.0.0.1
      port: 16379
      database: 0

其中,name 必须等于“RequestRateLimiter”内置限流过滤器,其他参数的含义如下:

  • redis-rate-limiter.replenishRate:令牌填充速度:每秒允许请求数。
  • redis-rate-limiter.burstCapacity:令牌桶容量:最大令牌数。
  • keyResolver:根据哪个 key 进行限流,它的值是 spEL 表达式。

SpEL(Spring Expression Language,Spring 表达式语言)是 Spring 框架中用于提供灵活、强大的表达式解析和求值功能的统一表达式语言。它可以在运行时动态地解析和求值字符串表达式,通常用于配置文件中的属性值、注解、XML 配置等地方。

注意事项

当 Spring Cloud Gateway 配合 Redis 实现限流的时候,它对于 Redis 的版本是有要求的,因为它在限流时调用了一个 Redis 高版本的函数,所以 Redis Server 版本太低,限流无效,Redis Server 最好是 5.X 以上。

限流测试

最后,我们频繁的访问就会看到如下限流信息:

image-20240628180750482

限流使用算法

Spring Cloud Gateway 内置限流功能使用的算法是令牌桶限流算法

令牌桶限流算法:令牌按固定的速率被放入令牌桶中,桶中最多存放 N 个令牌(Token),当桶装满时,新添加的令牌被丢弃或拒绝。当请求到达时,将从桶中删除 1 个令牌。令牌桶中的令牌不仅可以被移除,还可以往里添加,所以为了保证接口随时有数据通过,必须不停地往桶里加令牌。由此可见,往桶里加令牌的速度就决定了数据通过接口的速度。我们通过控制往令牌桶里加令牌的速度从而控制接口的流量。 令牌桶执行流程如下图所示:

image-20240628180918336

常见的限流算法还有:计数器算法、滑动计数器算法、漏桶算法等

限流实现原理:

Spring Cloud Gateway 执行过程如下图所示:

image-20240628181020321

从图中可以看出,所有的请求来了之后,会先走过滤器,只有过滤器通过之后,才能调用后续的内部微服务,这样我们就可以通过过滤器来控制微服务的调用,从而实现限流功能了。

Spring Cloud Gateway 过滤器是基于令牌桶算法来限制请求的速率,该过滤器根据配置的限流规则,在指定的时间窗口内分配一定数量的令牌,每个令牌代表一个允许通过的请求,当一个请求到达时,如果没有可用的令牌,则请求将被阻塞或拒绝。

令牌桶的执行过程如下:

  1. 初始化:在加载过滤器工厂时,会基于给定的限流规则创建一个限流器,该限流器包含了令牌桶算法的逻辑。默认情况下,令牌桶是按照固定速率进行填充,也可以配置为令牌桶按照令牌令牌的方式进行填充。
  2. 请求处理:每当有请求进来时,限流器会检查当前令牌桶中是否有可用的令牌。如果有可用的令牌,则请求会被放行,令牌桶中的令牌数量减少;如果没有可用的令牌,则请求会被阻塞或拒绝。
  3. 令牌桶填充:限流器会定期填充令牌桶,即向令牌桶中添加新的令牌。填充的速率取决于限流规则中配置的速率值。
  4. 令牌桶容量控制:限流器还会根据限流规则中配置的令牌桶容量,控制令牌桶中的令牌数量。如果令牌桶已满,则多余的令牌会被丢弃。

主流网关组件 Spring Cloud Gateway 实现限流的方式主要有两种:内置限流过滤器和外部限流组件,如 Sentinel、Hystrix 等。而最简单的限流功能,我们只需要使用 Spring Cloud Gateway 过滤器 + Redis 即可(实现),其使用的是令牌桶的限流算法来实现限流功能的。

如何防止短信盗刷和短信轰炸?

短信盗刷和短信轰炸是项目开发中必须要解决的问题之一,它的优先级不亚于 SQL 注入的问题,同时它也是面试中比较常见的一个经典面试题,今天我们就来看下,如何防止这个问题。

短信盗刷和短信轰炸的概念如下:

  • 短信盗刷是指使用某种技术手段,伪造大量手机号调用业务系统,盗取并发送大量短信的问题。这样会导致短信系统欠费,不能正常发送短信,同时也给业务系统方,带来了一定的经济损失和不必要的麻烦。
  • 短信轰炸是指攻击者利用某种技术手段,连续、大量地向目标手机号码发送短信,以达到骚扰、干扰或消耗目标用户的时间、流量与精力的目的。这种行为可能会对受害者造成骚扰、通信中断和手机电量消耗过快等问题。

短信盗刷和短信轰炸属于一类问题,可以一起解决。但这类问题的解决,不能依靠某一种解决方案,而是多种解决方案共同作用,来预防此类问题的发生。

这类问题的综合解决方案有以下几个:

  1. 添加图形验证码:用户发送短信前,需要先输入正确的图形验证码,或拖动验证码等验证,验证通过之后,才能正常发送短信验证码。因为图形验证码的破解难度非常大,所以就避免了自动发送短信程序的执行。
  2. 添加 IP 限制:对请求 IP 的发送次数进行限制,避免短信盗刷和短信轰炸的问题。例如,每个 IP 每天只能发送 10 条短信。
  3. 开启 IP 黑名单:限制某个 IP 短信发送功能,从而禁止自动发送短信程序的执行。
  4. 限制发送频次:一个手机号不能一直不停的发送验证码(即使更换了多个 IP 也不行),设置一个手机号,每分钟内只能发送 1 次验证码;一小时之内,只能发送 5 次验证码;一天之内,只能发送 10 次验证码。
  5. 开启短信提供商的防控和报警功能:几乎所有的短信提供商都提供了,异常短信的防控和提醒功能,开启这些保护措施,可以尽可能的避免短信盗刷的问题。

具体实现如下

添加图形验证码

图形验证码的执行流程如下:

  1. 当用户点击“发送短信验证码”的时候,前端程序请求后端生成图形验证码,后端程序生成图形验证码的功能,可以借助 Hutool 框架来生成,它的核心实现代码如下:
java
@RequestMapping("/getcaptcha")
public AjaxResult getCaptcha(){
    // 1.生成验证码到本地
    // 定义图形验证码的长和宽
    LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(128, 50);
    String uuid = UUID.randomUUID().toString().replace("-","");
    // 图形验证码写出,可以写出到文件,也可以写出到流
    lineCaptcha.write(imagepath+uuid+".png");
    // url 地址
    String url = "/image/"+uuid+".png";
    // 将验证码存储到 redis
    redisTemplate.opsForValue().set(uuid,lineCaptcha.getCode());
    HashMap<String,String> result = new HashMap<>();
    result.put("codeurl",url);
    result.put("codekey",uuid);
    return AjaxResult.succ(result);
}

上述执行代码中有两个关键操作:第一,生成图形验证码,并返回给前端程序;第二,将此图形验证的标识(ID)和正确的验证码保存到 Redis,方便后续验证。

  1. 前端用户拿到图形验证码之后,输入图形验证码,请求后端程序验证,并发送短信验证码。
  2. 后端程序拿到(图形)验证码之后,先验证(图形)验证码的正确性.如果正确,则发送短信验证码,否则,将不执行后续流程并返回执行失败给前端,核心实现代码如下:
java
// redis 里面 key 对应的真实的验证码
String redisCodeValue = (String) redisTemplate.opsForValue().get(user.getCodeKey());
// 验证 redis 中的验证码和用户输入的验证码是否一致
if (!StringUtils.hasLength(redisCodeValue) || !redisCodeValue.equals(user.getCheckCode())) {
    // 验证码不正确
    return AjaxResult.fail(-1, "验证码错误");
}
// 清除 redis 中的验证码
redisTemplate.opsForValue().set(userInfoVO.getCodeKey(), "");
// 请求短信 API 发送短信业务......

添加IP限制

IP 限制可以在网关层 Spring Cloud Gateway 中实现,在 Gateway 中使用全局过滤器来完成 IP 限制,核心实现代码如下:

java
@Component
public class IpFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取请求 IP
        String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
        Long count = redisTemplate.opsForValue().increment(ip, 1); // 累加值
        if(count >= 20){ // 大于最大执行次数
            redisTemplate.opsForValue().decrement(ip, 1); // 变回原来的值
            // 终止执行
            response.setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
            return response.setComplete();
        }
        redisTemplate.expire(ip, 1, TimeUnit.DAYS); // 设置过期时间
        // 执行成功,继续执行后续流程
        return chain.filter(exchange);
    }
}

其中,访问次数使用 Redis 来存储。

开启IP黑名单限制

IP 黑名单可以在网管层面通过代码实现,也可以通过短信提供商提供的 IP 黑名单来实现。

例如,阿里云短信提供的 IP 黑名单机制,如下图所示:

image-20240629164655575

image-20240629165110802

通过以上设置之后,我们就可以将监控中有问题的 IP 设置为黑名单,此时 IP 黑名单中的 IP 就不能调用短信的发送功能了。

PS:网关层面实现 IP 黑名单,可以参考添加 IP 限制的代码进行改造。

限制验证码的发送频次

验证码的发送频次,可以通过网关或短信提供商来解决。以阿里云短信为例,它可以针对每个手机号设置每分钟、每小时、每天的短信最大发送数

开启短信提供商的防控和报警功能

短信提供商通常会提供防盗、防刷的机制。例如,阿里短信它可以设置短信发送总量阈值报警、套餐余量提醒、每日/每月发送短信数量限制等功能

限流的实现方式有哪些?

限流是指在各种应用场景中,通过技术和策略手段对数据流量、请求频率或资源消耗进行有计划的限制,以避免系统负载过高、性能下降甚至崩溃的情况发生。限流的目标在于维护系统的稳定性和可用性,并确保服务质量。

使用限流的好处有以下几个:

  1. 保护系统稳定性:过多的并发请求可能导致服务器内存耗尽、CPU 使用率饱和,从而引发系统响应慢、无法正常服务的问题。
  2. 防止资源滥用:确保有限的服务资源被合理公平地分配给所有用户,防止个别用户或恶意程序过度消耗资源。
  3. 优化用户体验:对于网站和应用程序而言,如果任由高并发导致响应速度变慢,会影响所有用户的正常使用体验。
  4. 保障安全:在网络层面,限流有助于防范 DoS/DDoS 攻击,降低系统遭受恶意攻击的风险。
  5. 运维成本控制:合理的限流措施可以帮助企业减少不必要的硬件投入,节省运营成本。

在 Java 中,限流的实现方式有很多种,例如以下这些:

  1. 单机限流:使用 JUC 下的 Semaphore 限流,或一些常用的框架,例如 Google 的 Guava 框架进行限流,但这种限流方式都是基于 JVM 层面的内存级别的单台机器限流。
  2. 网关层限流:单机限流往往不适用于分布式系统,而分布式系统可以在网关层限流,如 Spring Cloud Gateway 通过 Sentinel、Hystrix 对整个集群进行限流。
  3. Nginx 限流:通常在网关层的上游,我们会使用 Nginx 一起来配合使用,也就是用户请求会先到 Nginx(或 Nginx 集群),然后再将请求转发给网关,网关再调用其他的微服务,从而实现整个流程的请求调用,因此 Nginx 限流也是分布式系统中常用的限流手段。

它们限流的具体实现如下。

单机限流

JVM 层面多线程级别的限流可以使用 JUC 下的 Semaphore,具体使用示例如下:

java
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreExample {
    // 只允许5个线程同时访问
    private final Semaphore semaphore = new Semaphore(5); 

    public void accessResource() {
        try {
            semaphore.acquire(); // 获取许可,如果当前许可数不足,则会阻塞
            System.out.println(Thread.currentThread().getName() + "获得了许可,正在访问资源...");
            // 模拟访问资源的时间消耗
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "访问资源结束,释放许可...");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        } finally {
            semaphore.release(); // 访问结束后释放许可
        }
    }

    public static void main(String[] args) {
        SemaphoreExample example = new SemaphoreExample();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> example.accessResource()).start();
        }
    }
}

想要实现更平滑的单机限流,可以考虑 Google 提供的 Guava 框架,它的使用示例如下。

首先在 pom.xml 添加 guava 引用,配置如下:

java
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>28.2-jre</version>
</dependency>

具体实现代码如下:

java
import com.google.common.util.concurrent.RateLimiter;
import java.time.Instant;

/**
 * Guava 实现限流
 */
public class RateLimiterExample {
    public static void main(String[] args) {
        // 每秒产生 10 个令牌(每 100 ms 产生一个)
        RateLimiter rt = RateLimiter.create(10);
        for (int i = 0; i < 11; i++) {
            new Thread(() -> {
                // 获取 1 个令牌,获取到令牌就执行,否则就阻塞等待
                rt.acquire();
                System.out.println("正常执行方法,ts:" + Instant.now());
            }).start();
        }
    }
}

网关层限流

在 Spring Cloud Gateway 网关层限流,可以借助 Sentinel 等限流框架来实现,它的实现步骤如下。

首先,在 pom.xml 中添加 Gateway 和 Sentinel 相关依赖,如下所示:

xml
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>

配置限流相关的规则,如下示例所示:

yaml
spring:
  application:
    name: gate-way-blog
  cloud:
    sentinel:
      transport:
        dashboard: localhost:18080
      scg: # 配置限流之后,响应内容
        fallback:
          # 两种模式,一种是 response 返回文字提示信息,
          # 另一种是 redirect 重定向跳转,不过配置 redirect 也要配置对应的跳转的 uri
          mode: response
          # 响应的状态
          response-status: 200
          # 响应体
          response-body: '{"code": -10,"message": "被熔断或限流!"}'

最后在 Sentinel 控制台配置网关的限流设置即可,当然也可以使用 Nacos 作为数据源,两者选择配置其中一个即可。

Nginx限流

Nginx 提供了两种限流手段:

  • 通过控制速率来实现限流。
  • 通过控制并发连接数来实现限流。

我们一个一个来看。

我们需要使用 limit_req_zone 用来限制单位时间内的请求数,即速率限制,示例配置如下:

xml
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
server {
  location / {
    limit_req zone=mylimit;
  }
}

以上配置表示,限制每个 IP 访问的速度为 2r/s,因为 Nginx 的限流统计是基于毫秒的,我们设置的速度是 2r/s,转换一下就是 500ms 内单个 IP 只允许通过 1 个请求,从 501ms 开始才允许通过第 2 个请求。

我们使用单 IP 在 10ms 内发并发送了 6 个请求的执行结果如下:

image-20240628223501658

从以上结果可以看出他的执行符合我们的预期,只有 1 个执行成功了,其他的 5 个被拒绝了(第 2 个在 501ms 才会被正常执行)。

速率限制升级版

上面的速率控制虽然很精准但是应用于真实环境未免太苛刻了,真实情况下我们应该控制一个 IP 单位总时间内的总访问次数,而不是像上面那么精确但毫秒,我们可以使用 burst 关键字开启此设置,示例配置如下:

xml
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
server {
  location / {
    limit_req zone=mylimit burst=4;
  }
}

burst=4 表示每个 IP 最多允许4个突发请求,如果单个 IP 在 10ms 内发送 6 次请求的结果如下:

image-20240628223625457

从以上结果可以看出,有 1 个请求被立即处理了,4 个请求被放到 burst 队列里排队执行了,另外 1 个请求被拒绝了。

控制并发数实现限流

利用 limit_conn_zone 和 limit_conn 两个指令即可控制并发数,示例配置如下:

xml
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
server {
  ...
  limit_conn perip 10;
  limit_conn perserver 100;
}

其中 limit_conn perip 10 表示限制单个 IP 同时最多能持有 10 个连接;limit_conn perserver 100 表示 server 同时能处理并发连接的总数为 100 个。

小贴士:只有当 request header 被后端处理后,这个连接才进行计数。

熔断和降级有什么区别?

熔断和降级都是系统自我保护的一种机制,但二者又有所不同,它们的区别主要体现在以下几点:

  1. 概念不同
  2. 触发条件不同
  3. 归属关系不同

概念不同

熔断概念

“熔断”一词早期来自股票市场。熔断(Circuit Breaker)也叫自动停盘机制,是指当股指波幅达到规定的熔断点时,交易所为控制风险采取的暂停交易措施。比如 2020 年 3 月 9 日,纽约股市开盘出现暴跌,随后跌幅达到 7% 上限,触发熔断机制,停止交易 15 分钟,恢复交易后跌幅有所减缓。

熔断在程序中,有“断开”的意思,当系统繁忙时,程序为了保证整体的稳定性,会暂时停止服务一段时间,以保证系统的稳定性。

如果没有熔断机制的话,会导致联机故障和服务雪崩等问题,如下图所示:

image-20240628225736292

降级概念

降级(Degradation)降低级别的意思,它是指程序在出现问题时,仍能保证有限功能可用的一种机制。

比如电商交易系统在双 11 时,使用的人比较多,此时如果开放所有功能,可能会导致系统不可用,所以此时可以开启降级功能,优先保证支付功能可用,而其他非核心功能,如评论、物流、商品介绍等功能可以暂时关闭。

所以,从上述信息可以看出:降级是一种退而求其次的选择,而熔断却是整体不可用

触发条件不同

不同框架的熔断和降级的触发条件是不同的,本文咱们以经典的 Spring Cloud 组件 Hystrix 为例,来说明触发条件的问题。

Hystrix 熔断触发条件

默认情况 hystrix 如果检测到 10 秒内请求的失败率超过 50%,就触发熔断机制。之后每隔 5 秒重新尝试请求微服务,如果微服务不能响应,继续走熔断机制。如果微服务可达,则关闭熔断机制,恢复正常请求。

image-20240628225919044

Hystrix 降级触发条件

默认情况下,hystrix 在以下 4 种条件下都会触发降级机制:

  1. 方法抛出 HystrixBadRequestException
  2. 方法调用超时
  3. 熔断器开启拦截调用
  4. 线程池或队列或信号量已满

虽然 hystrix 组件的触发机制,不能代表所有的熔断和降级机制,但足矣说明此问题。

归属关系不同

**熔断时可能会调用降级机制,而降级时通常不会调用熔断机制。**因为熔断是从全局出发,为了保证系统稳定性而停用服务,而降级是退而求其次,提供一种保底的解决方案,所以它们的归属关系是不同(熔断 > 降级)。

当然,某些框架如 Sentinel,它早期在 Dashboard 控制台中可能叫“降级”,但在新版中新版本又叫“熔断”,如下图所示:

image-20240628230059834

但在两个版本中都是通过同一个异常类型 DegradeException 来监听的,如下代码所示:

image-20240628230114680

所以,在 Sentinel 中,熔断和降级功能指的都是同一件事,也侧面证明了“熔断”和“降级”概念的相似性。但我们要知道它们本质上是不同的,就像两个双胞胎,不能因为他们长得像,就说他们是同一个人。

熔断和降级都是程序在我保护的一种机制,但二者在概念、触发条件、归属关系上都是不同的。熔断更偏向于全局视角的自我保护(机制),而降级则偏向于具体模块“退而请其次”的解决方案。

Nacos注册中心有几种调用方式?

什么是注册中心?

注册中心(Registry)是一种用于服务发现服务注册的分布式系统组件。它是在微服务架构中起关键作用的一部分,用于管理和维护服务实例的信息以及它们的状态。

它的执行流程如下图所示:

image-20240629121053106

注册中心充当了服务之间的中介和协调者,它的主要功能有以下这些:

  1. 服务注册:服务提供者将自己的服务实例信息(例如 IP 地址、端口号、服务名称等)注册到注册中心。通过注册中心,服务提供者可以将自己的存在告知其他服务。
  2. 服务发现:服务消费者通过向注册中心查询服务信息,获取可用的服务实例列表。通过注册中心,服务消费者可以找到并连接到需要调用的服务。
  3. 健康检查与负载均衡:注册中心可以定期检查注册的服务实例的健康状态,并从可用实例中进行负载均衡,确保请求可以被正确地转发到可用的服务实例。
  4. 动态扩容与缩容:在注册中心中注册的服务实例信息可以方便地进行动态的增加和减少。当有新的服务实例上线时,可以自动地将其注册到注册中心。当服务实例下线时,注册中心会将其从服务列表中删除。

使用注册中心有以下优势和好处:

  • 服务自动发现和负载均衡:服务消费者无需手动配置目标服务的地址,而是通过注册中心动态获取可用的服务实例,并通过负载均衡算法选择合适的实例进行调用。
  • 服务弹性和可扩展性:新的服务实例可以动态注册,并在发生故障或需要扩展时快速提供更多的实例,从而提供更高的服务弹性和可扩展性。
  • 中心化管理和监控:注册中心提供了中心化的服务管理和监控功能,可以对服务实例的状态、健康状况和流量等进行监控和管理。
  • 降低耦合和提高灵活性:服务间的通信不再直接依赖硬编码的地址,而是通过注册中心进行解耦,使得服务的部署和变更更加灵活和可控。

常见的注册中心包括 ZooKeeper、Eureka、Nacos 等。这些注册中心可以作为微服务架构中的核心组件,用于实现服务的自动发现、负载均衡和动态扩容等功能。

方法概述

当 Nacos 中注册了 Restful 接口时(一种软件架构风格,它是基于标准的 HTTP 协议和 URI 的一组约束和原则),其调用方式主要有以下两种:

  1. 使用 RestTemplate + Spring Cloud LoadBalancer
  2. 使用 OpenFeign + Spring Cloud LoadBalancer

RestTemplate+LoadBalancer调用

此方案的实现有以下 3 个关键步骤:

  1. 添加依赖:nacos + loadbalancer
  2. 设置配置文件
  3. 编写调用代码

具体实现如下。

xml
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

设置配置文件

yaml
spring:
  application:
    name: nacos-discovery-business
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
        register-enabled: false

编写调用代码

此步骤又分为以下两步:

  1. 给 RestTemplate 增加 LoadBalanced 支持
  2. 使用 RestTemplate 调用接口

RestTemplate添加LoadBalanced

在 Spring Boot 启动类上添加“@EnableDiscoveryClient”注解,并使用“@LoadBalanced”注解替换 IoC 容器中的 RestTemplate,具体实现代码如下:

java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient
public class BusinessApplication {
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(BusinessApplication.class, args);
    }
}

使用RestTemplate

java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/business")
public class BusinessController2 {
    @Autowired
    private RestTemplate restTemplate;
    @RequestMapping("/getnamebyid")
    public String getNameById(Integer id){
        return restTemplate.getForObject("http://nacos-discovery-demo/user/getnamebyid?id="+id,
                String.class);
    }
}

OpenFeign + LoadBalance调用

此步骤又分为以下 5 步:

  1. 添加依赖:nacos + openfeign + loadbalancer
  2. 设置配置文件
  3. 开启 openfeign 支持
  4. 编写 service 代码
  5. 调用 service 代码

具体实现如下。

添加依赖

xml
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
 <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
  </dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

设置配置文件

yaml
spring:
  application:
    name: nacos-discovery-business
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        username: nacos
        password: nacos
        register-enabled: false

开启OpenFeign

在 Spring Boot 启动类上添加 @EnableFeignClients 注解。

编写Service

java
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Service
@FeignClient(name = "nacos-producer") // name 为生产者的服务名
public interface UserService {
    @RequestMapping("/user/getinfo") // 调用生产者的接口
    String getInfo(@RequestParam String name);
}

调用Service

java
import com.example.consumer.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {
    @Autowired
    private UserService userService;
    @RequestMapping("/order")
    public String getOrder(@RequestParam String name){
        return userService.getInfo(name);
    }
}

注册中心作为微服务中不可或缺的重要组件,在微服务中充当着中介和协调者的作用。而 Nacos 作为近几年来,国内最热门的注册中心,其 Restf 接口调用有两种方式:RestTemplate + LoadBalancer 和 OpenFeign + LoadBalancer,开发者可以根据自己的实际需求,选择相应的调用方式。

常见的负载均衡策略

负载均衡策略是实现负载均衡器的关键,而负载均衡器又是分布式系统中不可或缺的重要组件。使用它有助于提高系统的整体性能、可用性、可靠性和安全性,同时支持系统的扩展和故障容忍性。对于处理大量请求的应用程序和微服务架构来说,负载均衡器是不可或缺的重要工具。

负载均衡分类

负载均衡分为服务器端负载均衡和客户端负载均衡。

  1. 服务器端负载均衡指的是存放在服务器端的负载均衡器,例如 Nginx、HAProxy、F5 等。

image-20240628225217776

  1. 客户端负载均衡指的是嵌套在客户端的负载均衡器,例如 Ribbon。

image-20240628225356860

常见负载均衡策略

但无论是服务器端负载均衡和客户端负载均衡,它们的负载均衡策略都是相同的,因为负载均衡策略本质上是一种思想。

常见的负载均衡策略有以下几个:

  1. 轮询(Round Robin):轮询策略按照顺序将每个新的请求分发给后端服务器,依次循环。这是一种最简单的负载均衡策略,适用于后端服务器的性能相近,且每个请求的处理时间大致相同的情况。
  2. 随机选择(Random):随机选择策略随机选择一个后端服务器来处理每个新的请求。这种策略适用于后端服务器性能相似,且每个请求的处理时间相近的情况,但不保证请求的分发是均匀的。
  3. 最少连接(Least Connections):最少连接策略将请求分发给当前连接数最少的后端服务器。这可以确保负载均衡在后端服务器的连接负载上均衡,但需要维护连接计数。
  4. IP 哈希(IP Hash):IP 哈希策略使用客户端的 IP 地址来计算哈希值,然后将请求发送到与哈希值对应的后端服务器。这种策略可用于确保来自同一客户端的请求都被发送到同一台后端服务器,适用于需要会话保持的情况。
  5. 加权轮询(Weighted Round Robin):加权轮询策略给每个后端服务器分配一个权重值,然后按照权重值比例来分发请求。这可以用来处理后端服务器性能不均衡的情况,将更多的请求分发给性能更高的服务器。
  6. 加权随机选择(Weighted Random):加权随机选择策略与加权轮询类似,但是按照权重值来随机选择后端服务器。这也可以用来处理后端服务器性能不均衡的情况,但是分发更随机。
  7. 最短响应时间(Least Response Time):最短响应时间策略会测量每个后端服务器的响应时间,并将请求发送到响应时间最短的服务器。这种策略可以确保客户端获得最快的响应,适用于要求低延迟的应用。

分布式事务二阶段和三阶段?

在分布式事务中,通常使用两阶段协议或三阶段协议来保障分布式事务的正常运行,它也是 X/Open 公司定义的一套解决分布式事务的规范。

X/Open 公司是由多家国际计算机厂商所组成的联盟组织,它建立之初是为了向 UNIX 环境提供标准。

分布式事务是指在分布式系统中,多个节点之间进行的事务操作。比如在分布式系统中,用户在下单时,需要同时创建订单信息和减库存的操作,然而创建订单信息和减库存是分布在不同服务器和不同数据库中的,如下图所示:

image-20240629113335273

此时我们就需要一个分布式事务介入,保证所有操作,要么一起提交,要么一起回滚。

两阶段提交

两阶段提交(Two-Phase Commit,简称 2PC)是一种分布式事务协议,确保所有参与者在提交或回滚事务时都处于一致的状态。2PC 协议包含以下两个阶段:

  1. 准备阶段(prepare phase):在这个阶段,事务协调者(Transaction Coordinator)向所有参与者(Transaction Participant)发出准备请求,询问它们是否准备好提交事务。参与者执行所有必要的操作,并回复协调者是否准备好提交事务。如果所有参与者都回复准备好提交事务,协调者将进入下一个阶段。如果任何参与者不能准备好提交事务,协调者将通知所有参与者回滚事务。
  2. 提交阶段(commit phase):在这个阶段,如果所有参与者都已准备好提交事务,则协调者向所有参与者发送提交请求。参与者执行所有必要的操作,并将其结果记录在持久性存储中。一旦所有参与者都已提交事务,协调者将向它们发送确认请求。如果任何参与者未能提交事务,则协调者将通知所有参与者回滚事务。

2PC 协议可以确保分布式事务的原子性和一致性,但是其效率较低,可能会出现阻塞等问题。因此,在实际应用中,可以使用其他分布式事务协议,如 3PC(Three-Phase Commit)Paxos 协议来代替。

两阶段提交问题

两阶段提交存在以下几个问题:

  1. 同步阻塞问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。也就是说从投票阶段到提交阶段完成这段时间,资源是被锁住的。
  2. 单点故障:由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。
  3. 数据不一致问题:在 2PC 最后提交阶段中,当协调者向参与者发送 commit 请求之后,发生了局部网络异常或者在发送 commit 请求过程中协调者发生了故障,这会导致只有一部分参与者接受到了 commit 请求。而在这部分参与者接到 commit 请求之后就会执行 commit 操作。但是其他部分未接到 commit 请求的机器则无法执行事务提交,于是整个分布式系统便出现了数据不一致性的现象。

三阶段提交

三阶段提交(Three-Phase Commit,简称3PC)是在 2PC 协议的基础上添加了一个额外的阶段来解决 2PC 协议可能出现的阻塞问题。 3PC 协议包含三个阶段:

  1. CanCommit 阶段(询问阶段):在这个阶段,事务协调者(Transaction Coordinator)向所有参与者(Transaction Participant)发出 CanCommit 请求,询问它们是否准备好提交事务。参与者执行所有必要的操作,并回复协调者它们是否可以提交事务。
  2. PreCommit 阶段(准备阶段):如果所有参与者都回复可以提交事务,则协调者将向所有参与者发送PreCommit 请求,通知它们准备提交事务。参与者执行所有必要的操作,并回复协调者它们是否已经准备好提交事务。
  3. DoCommit 阶段(提交阶段):如果所有参与者都已经准备好提交事务,则协调者将向所有参与者发送DoCommit 请求,通知它们提交事务。参与者执行所有必要的操作,并将其结果记录在持久性存储中。一旦所有参与者都已提交事务,协调者将向它们发送确认请求。如果任何参与者未能提交事务,则协调者将通知所有参与者回滚事务。

与 2PC 协议相比,3PC 协议将 CanCommit 阶段(询问阶段)添加到协议中,使参与者能够在 CanCommit 阶段发现并解决可能导致阻塞的问题。这样,3PC 协议能够更快地执行提交或回滚事务,并减少不必要的等待时间。需要注意的是,与 2PC 协议相比,3PC 协议仍然可能存在阻塞的问题。

两阶段提交VS三阶段提交

2PC 和 3PC 是分布式事务中两种常见的协议,3PC 可以看作是 2PC 协议的改进版本,相比于 2PC 它有两点改进:

  1. 引入了超时机制,同时在协调者和参与者中都引入超时机制(2PC 只有协调者有超时机制);
  2. 3PC 相比于 2PC 增加了 CanCommit 阶段,可以尽早的发现问题,从而避免了后续的阻塞和无效操作。

也就是说,3PC 相比于 2PC,因为引入了超时机制,所以发生阻塞的几率变小了;同时 3PC 把之前 2PC 的准备阶段一分为二,变成了两步,这样就多了一个缓冲阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。

数据一致性问题和解决方案

3PC 虽然可以减少同步阻塞问题和单点故障问题,但依然存在数据一致性问题(概率很小),而解决数据一致性问题的方案有很多,比如 Paxos 算法或柔性事务机制等。

Paxos 算法

Paxos 算法是一种基于消息传递的分布式一致性算法,并在 2013 年获得了图灵奖。 图灵奖(ACM A.M. Turing Award)是计算机科学领域最高荣誉之一,由美国计算机协会(ACM)于 1966 年设立,每年颁发一次,表彰对计算机科学领域做出杰出贡献的人士或团体。 简单来说,Paxos 算法是一种分布式共识算法,用于在分布式系统中实现数据的一致性和共识,保证分布式系统中不同节点之间的数据同步和一致性。 Paxos 算法由三个角色组成:提议者、接受者和学习者。当一个节点需要发起一个提议时,它会向其他节点发送一个提议,接受者会接收到这个提议,并对其进行处理,可能会拒绝提议,也可能会接受提议。如果有足够多的节点接受了该提议,那么提议就会被确定下来,并且通知给所有学习者,最终所有节点都会达成共识。 Paxos 算法看起来很简单,但它实际上是非常的复杂。 Paxos 算法应用的产品也很多,比如以下几个:

Redis:Redis 是一个内存数据库,使用 Paxos 算法实现了分布式锁服务和主从复制等功能。

MySQL:MySQL 5.7 推出的用来取代传统的主从复制的 MySQL Group Replication 等。

ZooKeeper:ZooKeeper 是一个分布式协调服务,使用 Paxos 算法实现了分布式锁服务和数据一致性等功能。

Apache Cassandra:Cassandra 是一个分布式数据库系统,使用 Paxos 算法实现了数据的一致性和复制等功能。

Google Chubby:Chubby 是 Google 内部使用的分布式锁服务,使用 Paxos 算法实现了分布式锁服务和命名服务等功能。

柔性事务

柔性事务机制:允许一定时间内不同节点的数据不一致,但要求最终一致的机制。 柔性事物有 TCC 补偿事物、可靠消息事物(MQ 事物)等。

在分布式事务中,通常使用两阶段或三阶段提交协议来保障分布式事务的正常执行。两阶段协议包含准备阶段和提交阶段,然而它存在同步阻塞问题、单点故障和数据一致性问题。

而三阶段协议可以看作是两阶段协议的改进版,它将两阶段的准备阶段一分为二,多了一个询问阶段,保证了提交阶段之前各参与节点的状态是一致的,同时引入了超时机制,减少了同步阻塞问题发生的几率。但 2PC 和 3PC 都存在数据一致性问题,此时可以采用 Paxos 算法或柔性事务机制等方案来解决事务一致性问题。

本网站支持IPV6 | Powered by XiaoSheng